﻿/*
 * Copyright (c) 2010 Code Owls LLC
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
 * associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 
 * subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial 
 * portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
*/
using System;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Text.RegularExpressions;
using System.Web.Security;
using ASPNETMembership.DynamicParameters;

namespace ASPNETMembership
{
    /// <summary>
    /// Provides access to the <see cref="Membership"/> functionality using standard PowerShell item cmdlets.
    /// </summary>
    [CmdletProvider("Membership", ProviderCapabilities.ShouldProcess | ProviderCapabilities.Filter)]
    public class Provider : ContainerCmdletProvider
    {
        /// <summary>
        /// Called in response to <code>new-psdrive</code>.
        /// </summary>
        /// <param name="drive">The base drive.</param>
        /// <returns>A <see cref="MembershipDriveInfo"/> object used to initialize the <see cref="Membership"/> type from script.</returns>
        protected override PSDriveInfo NewDrive( PSDriveInfo drive )
        {
            if( drive is MembershipDriveInfo )
            {
                return drive;
            }

            var driveParameters = this.DynamicParameters as DriveParameters;

            if( null == driveParameters )
            {
                var x = new ArgumentException("the dynamic parameter provided are not recognized", "NewDriveDynamicParameters");
                this.ThrowTerminatingError(new ErrorRecord(x, "InvalidDriveParameters", ErrorCategory.InvalidArgument, drive));
                
                return null;  
            }

            return new MembershipDriveInfo(drive, driveParameters);
        }

        /// <summary>
        /// Called in response to <code>new-psdrive</code> to obtain extra drive parameter information.
        /// </summary>
        /// <returns>A <see cref="DriveParameters"/> object.</returns>
        protected override object NewDriveDynamicParameters()
        {
            return new DriveParameters();
        }

        /// <summary>
        /// Determines whether the specified path is a valid path for this provider.
        /// </summary>
        /// <remarks>
        /// Valid path formats for this provider include the following:
        /// <list type="table">
        ///     <listheader>
        ///         <term>Path Type</term>
        ///         <description>Example</description>
        ///     </listheader>
        ///     <item><term>drive-qualified</term><definition>users:/username</definition>
        ///     <item><term>provider-qualified</term><definition>membership::username</definition>
        ///     <item><term>provider-internal</term><definition>username</definition>
        /// </list>
        /// </remarks>
        /// <param name="path">The path to test.</param>
        /// <returns>
        /// 	<c>true</c> if the specified path is valid; otherwise, <c>false</c>.
        /// </returns>
        protected override bool IsValidPath(string path)
        {
            return IsRootPath( path ) || ! String.IsNullOrEmpty( ExtractUserNameFromPath( path ) );
        }

        /// <summary>
        /// Called in response to <code>test-path</code>.
        /// </summary>
        /// <param name="path">The path of the item.</param>
        /// <returns><c>true</c> if an item exists at the specified path; otherwise <c>false</c>.</returns>
        protected override bool ItemExists( string path )
        {
            return IsRootPath( path ) || null != GetUserFromPath( path );
        }

        /// <summary>
        /// Called in response to <code>get-item</code> to write one or more items to the pipeline.
        /// </summary>
        /// <param name="path">The path of the item to retrieve.</param>
        protected override void GetItem(string path)
        {
            var users = GetUserCollectionFromPathOrFilter(path);

            if( null == users )
            {
                WriteNoUserFoundError( path );
                return;
            }

            foreach (var user in users)
            {
                WriteItemObject(user, path, false);
            }
        }

        /// <summary>
        /// Called in response to <code>set-item</code> to update an item in the provider.
        /// </summary>
        /// <param name="path">The path of the item to update.</param>
        /// <param name="value">The new value of the item.</param>
        protected override void SetItem(string path, object value)
        {
            var user = GetUserFromObject(value);

            if( null == user )
            {
                var x = new ArgumentException("expected MembershipUser value type");
                WriteError( new ErrorRecord( x, "InvalidArgument", ErrorCategory.InvalidArgument, value ));
                return;
            }

            // ensure that the username property of the MembershipUser object matches the path of the item being set
            if( user.UserName != ExtractUserNameFromPath( path ))
            {
                var x = new InvalidOperationException("you cannot copy or move a user to a new path");
                WriteError(new ErrorRecord(x, "ImmutableUserName", ErrorCategory.InvalidOperation, value));
                return;
            }

            Membership.UpdateUser( user );
        }

        /// <summary>
        /// Called in response to <code>new-item</code> to create a new item in the provider.
        /// </summary>
        /// <param name="path">The path of the item to create.</param>
        /// <param name="itemTypeName">Name of the item type.  This parameter is required by PowerShell, but is not used by this provider.</param>
        /// <param name="newItemValue">The new item value.  This parameter is required by PowerShell but is not used by this provider.</param>
        protected override void NewItem(string path, string itemTypeName, object newItemValue)
        {
            var userName = ExtractUserNameFromPath(path);
            if( String.IsNullOrEmpty( userName ))
            {
                var x = new ArgumentException("the path must include a username", "path");
                this.WriteError( new ErrorRecord( x, "InvalidUserPath", ErrorCategory.InvalidArgument, path));
                return;
            }

            if( null != GetUserFromPath( path ))
            {
                var x = new ArgumentException("the path specifies an item that already exists", "path");
                this.WriteError( new ErrorRecord( x, "InvalidUserPath", ErrorCategory.ResourceExists, path ));
                return;
            }

            var userParameters = this.DynamicParameters as UserParameters;

            if (null == userParameters)
            {
                var x = new ArgumentException("the dynamic parameters provided are not recognized", "NewItemDynamicParameters");
                this.ThrowTerminatingError(new ErrorRecord(x, "InvalidUserParameters", ErrorCategory.InvalidArgument, path));
                return;
            }

            var user = Membership.CreateUser(userName, userParameters.Password, userParameters.Email);
            WriteItemObject( user, path, false );

        }

        /// <summary>
        /// Called in response to <code>new-item</code> to obtain the dynamic parameters for creating a new <see cref="MembershipUser"/>.
        /// </summary>
        /// <param name="path">The path of the item being created.</param>
        /// <param name="itemTypeName">Name of the item type.</param>
        /// <param name="newItemValue">The new item value.</param>
        /// <returns></returns>
        protected override object NewItemDynamicParameters(string path, string itemTypeName, object newItemValue)
        {
            return new UserParameters();            
        }

        /// <summary>
        /// Called in response to <code>remove-item</code>.
        /// </summary>
        /// <param name="path">The path of the item to remove.</param>
        /// <param name="recurse">Not used in this provider.</param>
        protected override void RemoveItem(string path, bool recurse)
        {
            MembershipUserCollection users = GetUserCollectionFromPathOrFilter(path);
            if( null == users )
            {
                WriteNoUserFoundError(path);
                return;
            }

            foreach (MembershipUser user in users)
            {
                if (ShouldProcess(user.UserName))
                {
                    Membership.DeleteUser(user.UserName, true);
                }
            }
        }

        /// <summary>
        /// Called in response to <code>get-childitems</code>.
        /// </summary>
        /// <remarks>
        /// This provider supports native filtering.  The filtering mechanics are defined by the underlying Membership provider; e.g., <see cref="Membership.FindUsersByName(string)"/>
        /// <example>Both examples will filter the list of users to those those whose username starts with the letter 'b'; however, the first example may result in improved performance 
        /// since the filtering is done natively instead of in PowerShell, so fewer users will be returned from the membership store.
        ///     <code>get-childitems users:/ -filter b%</code>
        /// <code>get-childitems users:/ | where-object { $_.UserName.StartsWith( 'b' ) }</code>
        /// </example>
        /// </remarks>
        /// <param name="path">The path of the container item to enumerate.  For this provider, this must be the root of the provider drive.</param>
        /// <param name="recurse">Not used by this provider.</param>
        protected override void GetChildItems(string path, bool recurse)
        {
            if (!IsRootPath(path))
            {             
                return;
            }

            MembershipUserCollection users = null;

            
            if (!String.IsNullOrEmpty(this.Filter))
            {
                users = Membership.FindUsersByName(this.Filter);
            }
            else
            {
                users = Membership.GetAllUsers();
            }

            foreach (MembershipUser user in users)
            {
                WriteItemObject(user, user.UserName, false);
            }

        }

        /// <summary>
        /// Called to determine if the item at the specified path contains child items.  For this provider, this is only true for the item representing
        /// the root of the drive.
        /// </summary>
        /// <param name="path">The path of the container item to enumerate.  For this provider, this must be the root of the provider drive.</param>
        /// <returns>
        /// 	<c>true</c> if the item at the specified path contains child items; otherwise, <c>false</c>.
        /// </returns>
        protected override bool HasChildItems(string path)
        {
            return IsRootPath( path );
        }


        #region Helper Methods
        /// <summary>
        /// Determines whether the specified path represents the root of the provider drive.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>
        /// 	<c>true</c> if the specified path is the root of the drive; otherwise, <c>false</c>.
        /// </returns>
        static bool IsRootPath(string path)
        {
            path = Regex.Replace(path, @"^.+:[/\\]?", "");
            return String.IsNullOrEmpty(path);
        }

        /// <summary>
        /// Gets the <see cref="MembershipUser"/> from the supplied path.
        /// </summary>
        /// <param name="path">The path to resolve.</param>
        /// <returns>A <see cref="MembershipUser"/> object, or null if no user exists at the path.</returns>
        static MembershipUser GetUserFromPath(string path)
        {
            var username = ExtractUserNameFromPath( path );
            if( String.IsNullOrEmpty( username ))
            {
                return null;
            }
            return Membership.GetUser( username, false );
        }

        /// <summary>
        /// Extracts the user name from a path string.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>The user name extracted from the path.</returns>
        static string ExtractUserNameFromPath( string path )
        {
            if( String.IsNullOrEmpty( path ) )
            {
                return path;
            }

            // this regex matches all supported powershell path syntaxes:
            //  drive-qualified - users:/username
            //  provider-qualified - membership::users:/username
            //  provider-internal - users:/username
            var match = Regex.Match( path, @"(?:membership::)?(?:\w+:[\\/])?(?<username>.+)$", RegexOptions.IgnoreCase );
            if( match.Success )
            {
                return match.Groups[ "username" ].Value;
            }

            return String.Empty;
        }

        /// <summary>
        /// Attempts to resolve the object provided to a <see cref="MembershipUser"/> reference.
        /// </summary>
        /// <param name="value">The object to resolve.</param>
        /// <returns>A <see cref="MembershipUser"/> object, or null if the value does not represent a user.</returns>
        static MembershipUser GetUserFromObject(object value)
        {
            var user = value as MembershipUser;
            if (null == user)
            {
                PSObject psobject = value as PSObject;
                if (null != psobject)
                {
                    user = psobject.BaseObject as MembershipUser;
                }
            }
            return user;
        }

        /// <summary>
        /// Gets the single user that matches the specified path, or if the path 
        /// represents the root of the drive and a filter has been specified, the
        /// collection of users matching the native filter.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>A <see cref="MembershipUserCollection"/> containing one or more users that match the path or current filter, or null if no user matches the path specified.</returns>
        private MembershipUserCollection GetUserCollectionFromPathOrFilter(string path)
        {
            MembershipUserCollection users = null;

            if (IsRootPath(path) && !String.IsNullOrEmpty(Filter))
            {
                // *** if the -filter parameter is specified, then the Filter property will contain 
                //      the filter string from the user.  in this provider, the filter is passed to
                //      the sql LIKE operator; e.g., dir users: -filter user% will find all users 
                //      with a username starting with the string "user".
                users = Membership.FindUsersByName(Filter);
            }
            else
            {
                users = new MembershipUserCollection();
                var user = GetUserFromPath(path);

                if (null != user)
                {
                    users.Add(user);
                }
            }
            return users;
        }

        
        /// <summary>
        /// Writes an error to the current piptline that no user could be found at the path provided.
        /// </summary>
        /// <param name="path">The path.</param>
        private void WriteNoUserFoundError(string path)
        {
            var x = new ItemNotFoundException("no user at specified path");
            WriteError(new ErrorRecord(x, "NoUserAtPath", ErrorCategory.ObjectNotFound, path));
        }
        #endregion               
    }
}
